/*
 * File     : LibraryBuilder.java
 * Created  : 21 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.library;

import static com.googlecode.dni.internal.DniInternal.LOGGER;
import static com.googlecode.dni.internal.DniTypes.NATIVE_OBJECT_LIST_TYPE;
import static com.googlecode.dni.internal.DniTypes.POINTER_TYPE;
import static org.objectweb.asm.Opcodes.*;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import com.googlecode.dni.ByValue;
import com.googlecode.dni.CallingConvention;
import com.googlecode.dni.FixedSize;
import com.googlecode.dni.Function;
import com.googlecode.dni.IllegalLibraryException;
import com.googlecode.dni.Library;
import com.googlecode.dni.NullTerminated;
import com.googlecode.dni.Symbol;
import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.NativeUnsafeAccess;
import com.googlecode.dni.internal.call.JniNativeMethod;
import com.googlecode.dni.internal.call.NativeCallFactory;
import com.googlecode.dni.internal.structure.StructureRecord;
import com.googlecode.dni.type.NativeObjectList;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.structure.Structure;


/**
 * <p>
 *  Generates byte-code for implementations of library types.
 * </p>
 *
 * @param <T>
 *            the library interface type
 *
 * @author Matthew Wilson
 */
public final class LibraryBuilder< T >
{

    private final Class< T > type;

    private final Library library;

    private final CallingConvention conventions;

    private final String charset;

    /** The interface name (in byte-code style). */
    private final String interfaceName;

    /** The type implementation name (in byte-code style). */
    private final String implName;


    /**
     * Creates a new instance.
     *
     * @param <T>
     *            the library interface type
     * @param type
     *            the library interface type
     *
     * @return an instance
     */
    public static < T > LibraryBuilder< T > create( final Class< T > type )
    {
        if ( !type.isInterface() )
        {
            throw new IllegalLibraryException( "Library must an interface: " + type.getName() );
        }

        if ( type.getTypeParameters().length > 0 )
        {
            throw new IllegalLibraryException( "Library cannot have type parameters: " + type.getName() );
        }

        Library library = type.getAnnotation( Library.class );

        if ( library == null )
        {
            throw new IllegalLibraryException( "Library missing @Library annotation: " + type.getName() );
        }

        CallingConvention conventions = library.callingConvention();
        if ( !conventions.isSupported() )
        {
            throw new IllegalLibraryException( "Unsupported calling conventions: " + type.getName() + ": " + conventions );
        }
        if ( conventions == CallingConvention.PLATFORM_DEFAULT )
        {
            conventions = CallingConvention.getDefault();
        }

        return new LibraryBuilder< T >( type, library, conventions, library.charset() );
    }

    private LibraryBuilder( final Class< T > type,
                            final Library library,
                            final CallingConvention conventions,
                            final String charset )
    {
        this.type = type;
        this.library = library;
        this.conventions = conventions;
        this.charset = charset;

        this.interfaceName = this.type.getName().replace( '.', '/' );
        this.implName = this.interfaceName + "_Impl";
    }

    /**
     * Creates a library instance.
     *
     * @return a new instance
     */
    public T createLibrary()
    {
        LOGGER.info( "Creating Library implementation for: " + this.type.getName() );

        DynamicLibrary dynamicLibrary = DynamicLibrary.open( this.library.name(),
                                                             this.library.mangleName() );

        ClassWriter classWriter = new ClassWriter( ClassWriter.COMPUTE_FRAMES );

        classWriter.visit( V1_6,
                           ACC_PUBLIC | ACC_FINAL,
                           this.implName,
                           null,
                           "java/lang/Object",
                           new String[] { this.interfaceName } );


        NativeCallFactory nativeCallFactory = DniInternal.PLATFORM.createNativeCallFactory( this.conventions );
        nativeCallFactory.init( this.type, classWriter, this.implName );

        writeDefaultConstructor( classWriter );

        List< JniNativeMethod > nativeMethods = new ArrayList< JniNativeMethod >();

        boolean success = false;
        try
        {
            for ( Method method : this.type.getMethods() )
            {
                if ( method.getDeclaringClass() != Object.class )
                {
                    Symbol symbol = method.getAnnotation( Symbol.class );
                    if ( symbol != null )
                    {
                        writeSymbolMethod( dynamicLibrary, classWriter, symbol, method );
                    }
                    else
                    {
                        writeFunctionMethod( dynamicLibrary, nativeCallFactory, method, nativeMethods );
                    }
                }
            }
            success = true;
        }
        finally
        {
            if ( !success )
            {
                try
                {
                    dynamicLibrary.free();
                }
                catch ( Throwable throwable )
                {
                    DniInternal.LOGGER.log( Level.SEVERE, "Failed to free library on other exception", throwable );
                }
            }
        }

        nativeCallFactory.writeStaticInitialiser();

        classWriter.visitEnd();

        byte[] classFile = classWriter.toByteArray();
        Class< ? > implClass = DniInternal.defineClass( this.implName,
                                                        this.type.getClassLoader(),
                                                        classFile );

        // force the class to be initialised
        T instance;
        try
        {
            instance = this.type.cast( implClass.newInstance() );
        }
        catch ( InstantiationException exception )
        {
            throw new IllegalLibraryException( "Failed to create library", exception );
        }
        catch ( IllegalAccessException exception )
        {
            throw new IllegalLibraryException( "Failed to create library", exception );
        }

        // register all native methods
        for ( JniNativeMethod nativeMethod : nativeMethods )
        {
            nativeMethod.register( implClass );
        }

        return instance;
    }

    /**
     * Writes a default constructor.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeDefaultConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC, "<init>", "()V", null, null );
        methodVisitor.visitCode();

        // super();
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "<init>", "()V" );

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes out a function method.
     *
     * @param dynamicLibrary
     *            the dynamic library
     * @param nativeCallFactory
     *            the native call factory
     * @param method
     *            the Java method on the interface
     * @param nativeMethods
     *            destination list of native methods to register
     */
    private void writeFunctionMethod( final DynamicLibrary dynamicLibrary,
                                      final NativeCallFactory nativeCallFactory,
                                      final Method method,
                                      final List< JniNativeMethod > nativeMethods )
    {
        LOGGER.fine( "Creating Library method for: " + method );

        String name = method.getName();
        boolean mangle = true;
        String functionCharset = this.charset;

        Function function = method.getAnnotation( Function.class );
        if ( function != null )
        {
            name = function.name();
            if ( name.isEmpty() )
            {
                name = method.getName();
            }
            mangle = function.mangleName();

            functionCharset = function.charset();
            if ( functionCharset.isEmpty() || Function.LIBRARY_CHARSET.equals( functionCharset ) )
            {
                functionCharset = this.charset;
            }
        }

        // TODO mangling has to be done when all native parameters have been discovered...

        Pointer functionPointer;
        try
        {
            functionPointer = dynamicLibrary.getSymbol( name );
        }
        catch ( SymbolNotFoundException exception )
        {
            throw new IllegalLibraryException( "Cannot find function '" + name + "' in: " + this.type.getName(),
                                               exception );
        }

        JniNativeMethod nativeMethod = nativeCallFactory.writeMethod( method, functionPointer, functionCharset );
        if ( nativeMethod != null )
        {
            nativeMethods.add( nativeMethod );
        }
    }

    /**
     * Writes out a symbol method.
     *
     * @param dynamicLibrary
     *            the dynamic library
     * @param classWriter
     *            the class writer
     * @param symbol
     *             the symbol annotation
     * @param method
     *            the Java method on the interface
     */
    private void writeSymbolMethod( final DynamicLibrary dynamicLibrary,
                                    final ClassWriter classWriter,
                                    final Symbol symbol,
                                    final Method method )
    {
        // TODO this code is a mess

        String name = symbol.name();
        boolean byValue = method.getAnnotation( ByValue.class ) != null;
        if ( name.isEmpty() )
        {
            name = method.getName();
        }

        Pointer pointer;
        try
        {
            pointer = dynamicLibrary.getSymbol( name );
        }
        catch ( SymbolNotFoundException exception )
        {
            throw new IllegalLibraryException( "Cannot find symbol '" + name + "' in: " + this.type.getName(),
                                               exception );
        }

        if ( method.getParameterTypes().length > 0 )
        {
            throw new IllegalLibraryException( "Symbol method must take no parameters: " + method );
        }

        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC,
                                                               method.getName(),
                                                               Type.getMethodDescriptor( method ),
                                                               null,
                                                               null );

        methodVisitor.visitCode();

        Class< ? > returnType = method.getReturnType();
        java.lang.reflect.Type genericReturnType = method.getGenericReturnType();
        boolean handled = false;

        if ( returnType == Pointer.class )
        {
            long address = pointer.getAddress();
            if ( !byValue )
            {
                address = NativeUnsafeAccess.getPointer( address );
            }

            methodVisitor.visitTypeInsn( NEW, POINTER_TYPE );
            methodVisitor.visitInsn( DUP );
            methodVisitor.visitLdcInsn( address );
            methodVisitor.visitMethodInsn( INVOKESPECIAL,
                                           POINTER_TYPE,
                                           "<init>",
                                           "(J)V" );
            methodVisitor.visitInsn( ARETURN );

            handled = true;
        }
        else if ( Structure.class.isAssignableFrom( returnType ) )
        {
            // ensure the structure is loaded
            StructureRecord record =
                DniInternal.getStructureRecord( returnType.asSubclass( Structure.class ) );

            long address = pointer.getAddress();
            if ( !byValue )
            {
                address = NativeUnsafeAccess.getPointer( address );
            }

            String structType = record.getImplType().getName().replace( '.', '/' );
            methodVisitor.visitTypeInsn( NEW, structType );
            methodVisitor.visitInsn( DUP );
            methodVisitor.visitLdcInsn( address );
            methodVisitor.visitMethodInsn( INVOKESPECIAL,
                                           structType,
                                           "<init>",
                                           "(J)V" );
            methodVisitor.visitInsn( ARETURN );

            handled = true;
        }
        else if ( ( returnType == List.class || returnType == NativeObjectList.class )
                  && genericReturnType instanceof ParameterizedType)
        {
            FixedSize fixedSize = method.getAnnotation( FixedSize.class );
            NullTerminated nullTerminated = method.getAnnotation( NullTerminated.class );
            if ( fixedSize == null && nullTerminated == null )
            {
                throw new IllegalLibraryException( "List must be fixed size or null terminated: " + method );
            }

            java.lang.reflect.Type elementType = ( (ParameterizedType) genericReturnType ).getActualTypeArguments()[ 0 ];
            if ( elementType == Pointer.class )
            {
                long address = pointer.getAddress();
                if ( !byValue )
                {
                    address = NativeUnsafeAccess.getPointer( address );
                }

                methodVisitor.visitTypeInsn( NEW, POINTER_TYPE );
                methodVisitor.visitInsn( DUP );
                methodVisitor.visitLdcInsn( address );
                methodVisitor.visitMethodInsn( INVOKESPECIAL,
                                               POINTER_TYPE,
                                               "<init>",
                                               "(J)V" );

                methodVisitor.visitLdcInsn( fixedSize == null ? Integer.MAX_VALUE : fixedSize.value() );
                methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                               POINTER_TYPE,
                                               "getPointerList",
                                               "(I)L" + NATIVE_OBJECT_LIST_TYPE + ";" );

                if ( nullTerminated != null )
                {
                    methodVisitor.visitMethodInsn( INVOKESTATIC,
                                                   POINTER_TYPE,
                                                   "trimToNullTerminator",
                                                   "(L" + NATIVE_OBJECT_LIST_TYPE + ";)L" + NATIVE_OBJECT_LIST_TYPE + ";" );
                }

                methodVisitor.visitInsn( ARETURN );

                handled = true;
            }
        }

        if ( !handled )
        {
            throw new IllegalLibraryException( "Unsupported symbol return type: " + method );
        }

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

}
